<?php
namespace common\helpers;

/**
 * An easy way to convert xml to php array and convert php array to xml.
 * 
 * @author Xuegui Yuan <yuanxuegui@163.com>
 */
class XmlHelper
{

    /**
     * Parsing XML into array.
     *
     * @static
     *
     * @param string $contents
     *            string containing XML
     * @param bool $getAttributes            
     * @param bool $tagPriority
     *            priority of values in the array - `true` if the higher priority in the tag,
     *            `false` if only the attributes needed
     * @param string $encoding
     *            target XML encoding
     *            
     * @return array
     */
    public static function xmlToArray($contents, $getAttributes = true, $tagPriority = true, $encoding = 'utf-8')
    {
        $contents = trim($contents);
        if (empty($contents)) {
            return [];
        }
        $parser = xml_parser_create('');
        xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, $encoding);
        xml_parser_set_option($parser, XML_OPTION_CASE_FOLDING, 0);
        xml_parser_set_option($parser, XML_OPTION_SKIP_WHITE, 1);
        if (xml_parse_into_struct($parser, $contents, $xmlValues) === 0) {
            xml_parser_free($parser);
            return [];
        }
        xml_parser_free($parser);
        if (empty($xmlValues)) {
            return [];
        }
        unset($contents, $parser);
        $xmlArray = [];
        $current = &$xmlArray;
        $repeatedTagIndex = [];
        foreach ($xmlValues as $num => $xmlTag) {
            $result = null;
            $attributesData = null;
            if (isset($xmlTag['value'])) {
                if ($tagPriority) {
                    $result = $xmlTag['value'];
                } else {
                    $result['value'] = $xmlTag['value'];
                }
            }
            if (isset($xmlTag['attributes']) and $getAttributes) {
                foreach ($xmlTag['attributes'] as $attr => $val) {
                    if ($tagPriority) {
                        $attributesData[$attr] = $val;
                    } else {
                        $result['@attributes'][$attr] = $val;
                    }
                }
            }
            if ($xmlTag['type'] == 'open') {
                $parent[$xmlTag['level'] - 1] = &$current;
                if (! is_array($current) or (! in_array($xmlTag['tag'], array_keys($current)))) {
                    $current[$xmlTag['tag']] = $result;
                    unset($result);
                    if ($attributesData) {
                        $current['@' . $xmlTag['tag']] = $attributesData;
                    }
                    $repeatedTagIndex[$xmlTag['tag'] . '_' . $xmlTag['level']] = 1;
                    $current = &$current[$xmlTag['tag']];
                } else {
                    if (isset($current[$xmlTag['tag']]['0'])) {
                        $current[$xmlTag['tag']][$repeatedTagIndex[$xmlTag['tag'] . '_' . $xmlTag['level']]] = $result;
                        unset($result);
                        if ($attributesData) {
                            if (isset($repeatedTagIndex['@' . $xmlTag['tag'] . '_' . $xmlTag['level']])) {
                                $current[$xmlTag['tag']][$repeatedTagIndex['@' . $xmlTag['tag'] . '_' . $xmlTag['level']]] = $attributesData;
                            }
                        }
                        $repeatedTagIndex[$xmlTag['tag'] . '_' . $xmlTag['level']] += 1;
                    } else {
                        $current[$xmlTag['tag']] = [
                            $current[$xmlTag['tag']],
                            $result
                        ];
                        unset($result);
                        $repeatedTagIndex[$xmlTag['tag'] . '_' . $xmlTag['level']] = 2;
                        if (isset($current['@' . $xmlTag['tag']])) {
                            $current[$xmlTag['tag']]['@0'] = $current['@' . $xmlTag['tag']];
                            unset($current['@' . $xmlTag['tag']]);
                        }
                        if ($attributesData) {
                            $current[$xmlTag['tag']]['@1'] = $attributesData;
                        }
                    }
                    $lastItemIndex = $repeatedTagIndex[$xmlTag['tag'] . '_' . $xmlTag['level']] - 1;
                    $current = &$current[$xmlTag['tag']][$lastItemIndex];
                }
            } elseif ($xmlTag['type'] == 'complete') {
                if (! isset($current[$xmlTag['tag']]) and empty($current['@' . $xmlTag['tag']])) {
                    $current[$xmlTag['tag']] = $result;
                    unset($result);
                    $repeatedTagIndex[$xmlTag['tag'] . '_' . $xmlTag['level']] = 1;
                    if ($tagPriority and $attributesData) {
                        $current['@' . $xmlTag['tag']] = $attributesData;
                    }
                } else {
                    if (isset($current[$xmlTag['tag']]['0']) and is_array($current[$xmlTag['tag']])) {
                        $current[$xmlTag['tag']][$repeatedTagIndex[$xmlTag['tag'] . '_' . $xmlTag['level']]] = $result;
                        unset($result);
                        if ($tagPriority and $getAttributes and $attributesData) {
                            $current[$xmlTag['tag']]['@' . $repeatedTagIndex[$xmlTag['tag'] . '_' . $xmlTag['level']]] = $attributesData;
                        }
                        $repeatedTagIndex[$xmlTag['tag'] . '_' . $xmlTag['level']] += 1;
                    } else {
                        $current[$xmlTag['tag']] = [
                            $current[$xmlTag['tag']],
                            $result
                        ];
                        unset($result);
                        $repeatedTagIndex[$xmlTag['tag'] . '_' . $xmlTag['level']] = 1;
                        if ($tagPriority and $getAttributes) {
                            if (isset($current['@' . $xmlTag['tag']])) {
                                $current[$xmlTag['tag']]['@0'] = $current['@' . $xmlTag['tag']];
                                unset($current['@' . $xmlTag['tag']]);
                            }
                            if ($attributesData) {
                                $current[$xmlTag['tag']]['@' . $repeatedTagIndex[$xmlTag['tag'] . '_' . $xmlTag['level']]] = $attributesData;
                            }
                        }
                        $repeatedTagIndex[$xmlTag['tag'] . '_' . $xmlTag['level']] += 1;
                    }
                }
            } elseif ($xmlTag['type'] == 'close') {
                $current = &$parent[$xmlTag['level'] - 1];
            }
            unset($xmlValues[$num]);
        }
        return $xmlArray;
    }

    /**
     * convert array to xml
     * @public
     * 
     * @static
     *
     * @example arr2xml::convert(['root'=> ['data'=> ['value'=> 1]]]);
     * @example <root><data><value>1</value></data></root>
     * @example arr2xml::convert(['data'=> ['id'=> 'ID', 'info'=> null, '@info'=> ['attr'=> 'one'], 'more'=> [1, 2]]]);
     * @example <data><id>ID</id><info attr="one"></info><more>1</more><more>2</more></data>
     * @static
     *
     * @param array $arr
     *            - basis array for creating xml
     * @param bool $head
     *            - return with head tag
     * @return string
     */
    public static function convert(array $arr, $head = true)
    {
        return ($head ? '<?xml version="1.0" encoding="utf-8"?>' : '') . self::var2tag($arr);
    }

    /**
     * make tag from variable
     * @private
     * 
     * @static
     *
     * @param mixed $arr
     *            basis array for creating xml
     * @param int $tab
     *            depth level of elements
     * @return string
     */
    private static function var2tag($arr, $tab = 0)
    {
        if (! is_array($arr))
            return $arr;
        $xml = '';
        foreach ($arr as $key => $value) :
            if ($key{0} == '@') :
                $key = substr($key, 1);
                $xml .= "\n" . self::tab($tab) . '<' . $key . ' ' . self::arr2attr($value) . '>' . self::var2tag($arr[$key], $tab + 1) . '</' . $key . '>';
             elseif (! is_null($value)) :
                if (is_array($value) and array_key_exists(0, $value)) :
                    $xml .= "\n";
                    foreach ($value as $val)
                        $xml .= self::tab($tab) . '<' . $key . '>' . $val . '</' . $key . '>' . "\n";
                 else :
                    if (is_array($value) and ! array_key_exists('@' . $key, $value))
                        $xml .= "\n" . self::tab($tab) . '<' . $key . '>' . self::var2tag($value, $tab + 1) . self::tab($tab) . '</' . $key . '>' . "\n";
                    else
                        $xml .= "\n" . self::tab($tab) . '<' . $key . '>' . $value . '</' . $key . '>' . "\n";
                endif;
            endif;
        endforeach
        ;
        return $xml;
    }

    /**
     * repeat tab symbol
     * @private
     * 
     * @static
     *
     * @param int $tab
     *            times repeated
     * @return string
     */
    private static function tab($tab)
    {
        return str_repeat("\t", $tab);
    }

    /**
     * array to string with attributes
     * @private
     * 
     * @static
     *
     * @example arr2xml::arr2attr(['name'=>'my_input', 'placeholder'=> 'input here...']);
     * @param array $attr
     *            array with attributes
     * @return string
     */
    private static function arr2attr(array $attr)
    {
        $attr_str = '';
        foreach ($attr as $property => $value) :
            if (is_null($value))
                continue;
            if (is_numeric($property))
                $property = $value;
            $attr_str .= $property . '="' . $value . '" ';
        endforeach
        ;
        return trim($attr_str);
    }
}